Pipeline the dispatch lane: depth-4 ring, per-job timeline retirement (#151, #149) - #166
Merged
Merged
Conversation
SnowCheetos
force-pushed
the
codex/xdna-pipelined-dispatch
branch
from
August 28, 2026 07:02
bb4d9e2 to
d91081f
Compare
SnowCheetos
approved these changes
Aug 28, 2026
…#151, #149) The worker previously held the stream mutex across hrx_stream_dispatch plus a blocking hrx_stream_synchronize, one job at a time: one submission outstanding, zero overlap, and allocate_buffer queued behind every running dispatch. The lane now keeps up to four jobs in flight. Dispatch (record + flush + timeline position) holds the stream mutex briefly; the completion wait blocks on the stream's timeline semaphore in bounded slices with no lock held. Ring capacity is released in finish() before the terminal state becomes observable, so a caller that polls Complete and immediately resubmits never bounces off a stale count. Every fault-tier semantic maps one-to-one: a definite error still latches Failed and poisons; an untrusted boundary still leaves events pending and gates armed; on a tier-2 wedge the worker parks forever holding every in-flight job's retained resources and the stream, the same quarantine as before. Two on-metal findings shaped the implementation. The flushed batch's timeline value is assigned asynchronously: a position read immediately after the flush occasionally still reports the previous batch's target, and waiting on that retires early - observed as stale outputs (the max-pool NaN round reading the previous submission's bytes, roughly once per six cold runs). The stream is instance-private and dispatches are serialized, so each job's tick is the first value observed past its predecessor's; the position read now spins on that induction, watchdog-guarded. Second, throughput: pipelined and sequential submission measure identically (73-75 vs 69-75 microseconds per inference), and a batched-flush variant measured the same, so the per-submission floor is per-command driver/firmware round-trip cost inside one hardware context. docs/performance.md records the numbers; the honest conclusion is that further host-side submission restructuring cannot move the floor, and effective throughput comes from more work per dispatch (#151 steps 5-6) or parallel contexts (#121). The depth still pays for itself in semantics: multiple pending events (reference parity), submissions overlapping host-side polling and readback, and completion waits that no longer serialize allocate_buffer. New coverage, all on metal: a pipelined-payload test keeps four distinct submissions in flight for eight rounds and verifies every completion carries exactly its own payload; a deterministic ring-capacity test fills all four event slots, proves Busy at five, and proves reclaim restores exactly one; the pipelined-throughput benchmark reports amortized per-inference cost with oracle-validated warmups. The previously racy pending-release test now uses a new test-control HoldDispatch fault (a bounded delay before one healthy dispatch) instead of hoping the release beats real completion - pipelining made that race losable, observed twice in cold runs. hrx_stream_synchronize is no longer referenced and leaves the FFI; hrx_stream_flush, hrx_stream_get_timeline_position, and hrx_semaphore_wait join it, transcribed from the pinned headers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SnowCheetos
force-pushed
the
codex/xdna-pipelined-dispatch
branch
from
August 28, 2026 07:04
d91081f to
261058c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Second tranche of #151 (shared with #149): amortize the ~70 µs per-submission floor by keeping
several submissions in flight. Stacked on #162.
What changed. The worker held the stream mutex across
hrx_stream_dispatchplus a blockinghrx_stream_synchronize, one job at a time — one outstanding submission, zero overlap, andallocate_bufferqueued behind every running dispatch (a finding from the map-closeout review).The lane now keeps up to four jobs in flight: dispatch (record + flush + timeline position)
holds the stream mutex briefly, and the completion wait blocks on the stream's timeline semaphore
in bounded slices with no lock held. Every fault-tier semantic maps one-to-one; on a tier-2
wedge the worker parks forever holding every in-flight job's retained resources — the same
quarantine as before.
hrx_stream_synchronizeleaves the FFI;hrx_stream_flush,hrx_stream_get_timeline_position, andhrx_semaphore_waitjoin it, transcribed from the pinnedheaders.
A real driver behavior found on metal. The flushed batch's timeline value is assigned
asynchronously: a position read immediately after the flush occasionally still reports the
previous batch's target, and waiting on that retires a job early — observed as stale outputs
(the max-pool NaN round reading the previous submission's bytes, ~1 in 6 cold runs). Since the
stream is instance-private and dispatches are mutex-serialized, each job's tick is provably the
first timeline value observed past its predecessor's; the position read now spins on that
induction, watchdog-guarded. Six consecutive cold suite runs clean after the fix (two logged
failing runs before it).
The honest throughput result. Pipelined and sequential submission measure identically —
73.3–74.9 µs amortized vs 69.3–74.6 µs p50 across three runs each — and a batched-flush variant
(all in-flight dispatches under one
hrx_stream_flush) measured 70.1 µs, also identical. Thefloor is per-command driver/firmware round-trip cost inside one hardware context; no host-side
submission restructuring moves it.
docs/performance.mdrecords this, and it redirects theremaining #151/#149 throughput work to more work per dispatch (larger envelopes, striping — #151
steps 5–6) and parallel hardware contexts (#121). The depth still pays for itself in semantics:
multiple pending events (reference parity — OpenVINO has always allowed them), submissions
overlapping host-side polling and readback, and completion waits that no longer serialize
allocate_buffer.Coverage
All on metal (
1022:17f0, v2026.08 toolchain), fail-loud underVIRTIO_ACCEL_XDNA_REQUIRE_HARDWARE=1:pipelined_submissions_complete_in_order_with_their_own_payloads— four distinct submissions inflight for eight rounds; every completion must carry exactly its own submission's payload.
ring_capacity_bounds_outstanding_events_and_reclaim_restores_it— deterministic replacementfor the old depth-1 concurrent-submit test (whose Busy assertion is a race at depth 4): fills
all four event slots, proves
Busyat five, proves reclaiming one restores exactly one.measures_pipelined_int8_matmul_throughput(ignored, manual) — amortized per-inference costover 400 completions, warmups oracle-validated.
pending_releases_return_the_same_live_resources_for_retrynow uses a new test-controlHoldDispatchfault (bounded delay before one healthy dispatch) for a deterministic pendingwindow: pipelining made the old version's implicit race losable — observed twice in logged cold
runs before the rewrite.
Suite results: 29 passed / 0 failed (test-control) and 26 / 0 (default), including two final cold
runs of each after the last change; the shared conformance suite passes; the sequential benchmark
is unregressed (72.3 µs vs 72.1 µs on #162).
Compatibility
Guest-visible change: up to four submissions are now accepted before
Busy(previously one). TheBusy-on-full contract, event semantics, and all fault semantics are unchanged. Buffers stilladmit at most one outstanding submission each (the in-flight gates are unchanged), so pipelining
requires distinct buffer sets — the documented pattern.
Checklist
behavior — the ring depth is a documented capacity, not negotiated.
XdnaTestFault::HoldDispatchisdoc(hidden)behindtest-control).unsafechanges are confined toffi.rsdeclarations transcribed from the pinned headersand the worker's dispatch/wait calls, each with a local safety argument; SAFETY.md's
concurrency section is rewritten to match the new model.
Verification
On metal: hardware suite cold and warm in both feature configurations (logs kept for every run,
including the two pre-fix failures that motivated the timeline-induction change and the
HoldDispatch rewrite), conformance, and all three benchmarks. A peer session was using the NPU
concurrently during some runs; the reported benchmark spreads are across three runs each.
🤖 Generated with Claude Code